import { Plugin, resolvePackageData, resolvePackageEntry } from 'vite';
import { ManualChunksOption, GetModuleInfo, GetManualChunk } from 'rollup';
import * as path from 'path';

/**
 * 分割策略
 */
type Strategy = 'default' | 'all-in-one' | 'single-vendor' | 'unbundle';

/**
 * 自定义 chunk 分割规则
 */
type CustomSplitting = Record<string, (string | RegExp[])[]>;

/**
 * 插件入参
 */
interface ChunkSplit {
  /** 策略 */
  strategy?: Strategy;
  /** 自定义分割逻辑 */
  customSplitting?: CustomSplitting;
}

/** 判断是否为 CSS 相关模块 */
const isCSSIdentifier = (request: string): boolean =>
  /\\.(css|less|sass|scss|styl|stylus|pcss|postcss)($|\\?)/.test(request);

/**
 * 判断 id 是否为静态模块（页面初始化需要的）
 * @param id 模块 id
 * @param getModuleInfo 用于获取模块的信息
 * @param cache 缓存对象
 * @param importChain 引用链
 */
function staticImportedScan(
  id: string,
  getModuleInfo: GetModuleInfo,
  cache: Map<string, boolean> = new Map(),
  importChain: string[] = []
): boolean {
  // 取缓存数据
  if (cache.has(id)) {
    return cache.get(id);
  }

  // 存在循环引用
  if (importChain.includes(id)) {
    cache.set(id, false);
    return false;
  }

  const mod = getModuleInfo(id);
  if (!mod) {
    // 没有获取到模块信息
    cache.set(id, false);
    return false;
  }

  // 是否为用户或插件定义的入口模块
  if (mod.isEntry) {
    cache.set(id, true);
    return true;
  }

  // 向上递归查找模块引用
  const staticImport = mod.importers.some((importer) =>
    staticImportedScan(importer, getModuleInfo, cache, importChain.concat(id))
  );
  cache.set(id, staticImport);

  return staticImport;
}

/**
 * 获取模块入口文件的路径信息
 * @param name 模块名称
 * @example
 * ```
 * // D:/Develop/Workspace/study-record/vite-study/node_modules/react/index.js
 * resolveEntry('react')
 * ```
 */
const resolveEntry = (name: string): string =>
  resolvePackageEntry(
    name,
    resolvePackageData(name, process.cwd(), true),
    true,
    {
      isBuild: true,
      isProduction: process.env.NODE_ENV === 'production',
      isRequire: false,
      root: process.cwd(),
      preserveSymlinks: false,
    }
  );

/** 模块引用缓存对象 */
const cache = new Map<string, boolean>();

/**
 * 用来判断是否为某个模块的间接依赖
 * @param manualChunks rollup output.manualChunks 的选项值
 * @param customOptions 自定义分割逻辑
 * @returns output.manualChunks
 */
const wrapCustomSplitConfig = (
  manualChunks: GetManualChunk,
  customOptions: CustomSplitting
): ManualChunksOption => {
  /** 模块入口文件路径 */
  const depsInGroup: Record<string, string[]> = {};

  const groups = Object.keys(customOptions);
  for (const group of groups) {
    // 获取自定义规则的 value
    const packageInfo = customOptions[group];

    depsInGroup[group] = packageInfo
      // 筛选出值为字符串的，也就是模块名称
      .filter((item): boolean => typeof item === 'string')
      .map((item) => resolveEntry(item as string));
  }

  // 返回 manualChunks 处理函数
  return (moduleId, { getModuleIds, getModuleInfo }) => {
    /**
     *
     * @param id 模块 id
     * @param depPaths 模块文件路径
     * @param importChain 引用链
     * @returns 是否命中 depPaths 中的路径，存在依赖关系
     */
    const isDepInclude = (
      id: string,
      depPaths: string[],
      importChain: string[]
    ): boolean | undefined => {
      const key = `${id}-${depPaths.join('|')}`;

      /**
       * 存在循环引用，不能打包到一起
       * 例如 importChain = [A, B, C]，而当前 id 为 A，则产生了循环依赖
       */
      if (importChain.includes(id)) {
        cache.set(key, false);
        return false;
      }

      // 取缓存数据
      if (cache.has(key)) {
        return cache.get(key);
      }

      // 命中 depPaths 中的包，存在依赖关系，可以打包到一起
      if (depPaths.includes(id)) {
        importChain.forEach((item) =>
          cache.set(`${item}-${depPaths.join('|')}`, true)
        );
        return true;
      }

      const moduleInfo = getModuleInfo(id);
      // 没有获取到模块信息 || 模块没有被引用
      if (!moduleInfo || !moduleInfo.importers) {
        cache.set(key, false);
        return false;
      }

      /**
       * 向上递归查找引用者，看看该模块是不是命中 depPaths 中的包
       * importers 为当前模块的引用者列表
       */
      const isInclude = moduleInfo.importers.some((importer) =>
        isDepInclude(importer, depPaths, importChain.concat(id))
      );
      cache.set(key, isInclude);

      return isInclude;
    };

    for (const group of groups) {
      const deps = depsInGroup[group];
      /** 自定义的分割策略信息 */
      const packageInfo = customOptions[group];

      // 非 CSS 相关模块才进行处理
      if (!isCSSIdentifier(moduleId)) {
        // 是第三方依赖 && 该模块被 deps 中的包进行了引用
        if (
          moduleId.includes('node_modules') &&
          isDepInclude(moduleId, deps, [])
        ) {
          return group;
        }

        // 正则表达式的处理
        for (const rule of packageInfo) {
          // 符合正则匹配，则打包到 group chunk 中
          if (rule instanceof RegExp && rule.test(moduleId)) {
            return group;
          }
        }
      }
    }

    // 默认 chunk 处理
    return manualChunks(moduleId, { getModuleIds, getModuleInfo });
  };
};

/**
 * chunk 分割插件
 */
export default function chunkSplit(params?: ChunkSplit): Plugin {
  const { strategy = 'default', customSplitting = {} } = params;
  let manualChunks: ManualChunksOption;

  // 所有文件打包到一起，也就是一个 chunk
  if (strategy === 'all-in-one') {
    manualChunks = wrapCustomSplitConfig(() => {
      return null;
    }, customSplitting);
  }

  // 默认拆包方式
  if (strategy === 'default') {
    manualChunks = wrapCustomSplitConfig((id, { getModuleInfo }) => {
      if (id.includes('node_modules') && !isCSSIdentifier(id)) {
        if (staticImportedScan(id, getModuleInfo)) {
          return 'vendor';
        }
      }
    }, customSplitting);
  }

  // 实现不打包的效果，一个文件一个 chunk
  if (strategy === 'unbundle') {
    manualChunks = wrapCustomSplitConfig(
      (id, { getModuleInfo }): string | undefined => {
        // 针对第三方的包，将静态模块和动态模块进行拆分
        if (id.includes('node_modules') && !isCSSIdentifier(id)) {
          if (staticImportedScan(id, getModuleInfo)) {
            return 'vendor';
          } else {
            return 'async-vendor';
          }
        }

        const cwd = process.cwd();
        // 不是第三方包 && 不是 CSS 相关模块
        if (!id.includes('node_modules') && !isCSSIdentifier(id)) {
          const extname = path.extname(id);
          return path.relative(cwd, id).replace(extname, '');
        }

        return;
      },
      customSplitting
    );
  }

  return {
    name: 'vite-plugin-chunk-split',
    config() {
      return {
        build: {
          rollupOptions: {
            output: {
              manualChunks,
            },
          },
        },
      };
    },
  };
}
